home *** CD-ROM | disk | FTP | other *** search
- /**
- * Copyright (c) 2008, Jose Enrique Bolanos, Jorge Villalobos
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * * Neither the name of Jose Enrique Bolanos, Jorge Villalobos nor the names
- * of its contributors may be used to endorse or promote products derived
- * from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
- * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- **/
-
- var EXPORTED_SYMBOLS = [];
-
- const Cc = Components.classes;
- const Ci = Components.interfaces;
-
- Components.utils.import("resource://firefm/fmCommon.js");
- Components.utils.import("resource://firefm/fmSecret.js");
-
- // Observer topic for changed cookies. We use this to detect if we're logged in
- // with Last.FM or not.
- const TOPIC_COOKIE_CHANGED = "cookie-changed";
-
- // Base 64 characters.
- const BASE64 =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
-
- // The data used to store and retrieve the Last.fm API session key.
- const API_SESSION_LOGIN_HOST = "chrome://firefm";
- const API_SESSION_LOGIN_REALM = "Last.fm Web Services";
-
- // Last.fm URLs.
- const URL_BASE = "http://www.last.fm";
- const URL_BASE_SSL = "https://www.last.fm";
- const URL_EXT_BASE = "http://ext.last.fm";
-
- const URL_RPC = URL_EXT_BASE + "/1.0/webclient/xmlrpc.php";
- const URL_HANDSHAKE = URL_EXT_BASE + "/1.0/radio/webclient/handshake.php";
-
- // Data payloads for different API calls.
- const PARAMS_GET_SESSION =
- "<methodCall><methodName>getSession</methodName><params /></methodCall>";
- const PARAMS_HANDSHAKE = "?sessionKey=$(SESSION)&user=$(USER)";
- const PARAMS_ADJUST = "?lang=en&session=$(SESSION)&url=$(URL)&user=$(USER)";
- const PARAMS_PLAYLIST = "?sk=$(SESSION)&fod=true&y=$(TIMESTAMP)";
-
- // Regular expression to extract information from the handshake response.
- const RE_RESPONSE_HANDSHAKE =
- /^session\=([^\&]+)\&playlist\_url\=([^\&]+)\&subscriber\=([^\&]+)\&base\_url\=([^\&]+)\&base\_path\=([^\&]+)\&$/;
-
- // The amount of time to wait to check the logged in state at startup.
- const LOGGED_IN_TIMEOUT = 4 * 1000; // 4 seconds.
- // The amount of time to wait to show the API notification.
- const API_NOTIFICATION_TIMEOUT = 7 * 1000; // 7 seconds.
- // The amount of time to wait before doing the getSession call after checking
- // the Last.fm cookie.
- const GET_SESSION_TIMEOUT = 250; // 250 ms.
-
- /**
- * Handles the authentication against the Last.fm API, and the fetching of
- * playlists.
- */
- FireFM.Login = {
- // Topic notifications sent from this object.
- get TOPIC_USER_AUTHENTICATION() { return "firefm-user-authentication"; },
-
- /* Home URL. */
- get URL_HOME() { return URL_BASE; },
- /* Login URL. */
- get URL_LOGIN() {
- let url = URL_BASE_SSL + "/login";
-
- // point to the API permission page when doing the first login, or when
- // trying to login and there's already a user logged in.
- if (!this._useNormalLogin || (null != this._userName)) {
- url = this.URL_API_ACCESS;
- this._useNormalLoginPref.value = true;
- this._useNormalLogin = true;
- }
-
- return url;
- },
-
- /* Logout URL. */
- get URL_LOGOUT() { return URL_BASE + "/login/logout"; },
- /* API access approval URL. */
- get URL_API_ACCESS() {
- if (null == this._apiAccessURL) {
- this._apiAccessURL =
- (URL_BASE + "/api/auth?api_key=" + FireFM.Secret.API_KEY);
- }
-
- return this._apiAccessURL;
- },
-
- /* Regular expression that identifies the API access URL for all localizations
- of the Last.fm site. */
- get URL_API_ACCESS_RE() {
- if (null == this._apiAccessURLRE) {
- this._apiAccessURLRE =
- new RegExp(
- ("^http(?:s)?://(?:[a-z]+\\.)?last(?:\\.)?fm(?:[a-z\\.]+)?/api/auth" +
- "\\?api_key=" + FireFM.Secret.API_KEY),
- "i");
- }
-
- return this._apiAccessURLRE;
- },
-
- /* Login Manager service reference. */
- _loginManager : null,
- /* Logger for this object. */
- _logger : null,
- /* The name of the currently logged in user. */
- _userName : null,
- /* The api session of the currently logged in user. */
- _apiSession : null,
- /* Preference that indicates if the login URL should point to its normal
- location or the web services page where the user can enable Fire.fm use. */
- _useNormalLoginPref : null,
- /* Current value of the normal login URL preference. */
- _useNormalLogin : false,
- /* The API access approval URL. */
- _apiAccessURL : null,
- /* The API access approval URL regular expression. */
- _apiAccessURLRE : null,
-
- /**
- * Initializes this object.
- */
- init : function() {
- let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- let that = this;
-
- this._logger = FireFM.getLogger("FireFM.Login");
- this._logger.debug("init");
-
- this._loginManager =
- Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
-
- this._useNormalLoginPref =
- FireFM.Application.prefs.get(FireFM.PREF_BRANCH + "useNormalLoginURL");
- this._useNormalLogin = this._useNormalLoginPref.value;
-
- // set the logged in state.
- // XXX: we do this in a timeout to prevent the Master Password prompt from
- // appearing before the initial Firefox window.
- timer.initWithCallback(
- { notify : function() {
- that._checkLoggedInState();
- FireFM.obsService.addObserver(that, TOPIC_COOKIE_CHANGED, false);
- } },
- LOGGED_IN_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
- },
-
- /**
- * Gets the user name of the currently logged in user.
- * @return the user name of the currently logged in user. null if no user is
- * online.
- */
- get userName() {
- this._logger.debug("[getter] userName");
-
- return this._userName;
- },
-
- /**
- * Get the API session key for the currently logged in user.
- * @return the API session key for the currently logged in user. null if no
- * user is logged in or no key is stored for this user.
- */
- get apiSession() {
- this._logger.debug("[getter] apiSession");
-
- if (null != this._userName) {
- if (null == this._apiSession) {
- let userLower = this._userName.toLowerCase();
- let loginObjs = [];
- let loginCount;
-
- try {
- loginObjs =
- this._loginManager.findLogins(
- {}, API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM);
- } catch (e) {
- this._logger.warn(
- "[getter] apiSession. User rejected Master Password prompt.\n" + e);
- }
-
- loginCount = loginObjs.length;
- this._logger.debug("[getter] apiSession. Login count: " + loginCount);
-
- for (let i = 0; i < loginCount; i++) {
- if (userLower == loginObjs[i].username.toLowerCase()) {
- this._apiSession = loginObjs[i].password;
- break;
- }
- }
- }
- } else {
- this._apiSession = null;
- }
-
- return this._apiSession;
- },
-
- /**
- * Set the API session key for the currently logged in user.
- * @param aValue the API session key for the currently logged in user.
- */
- set apiSession(aValue) {
- this._logger.debug("[setter] apiSession");
-
- let nsLoginInfo =
- new Components.Constructor(
- "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
- let loginObj =
- new nsLoginInfo(
- API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM, this._userName,
- aValue, "", "");
-
- try {
- this._loginManager.addLogin(loginObj);
- } catch (e) {
- this._logger.warn(
- "[setter] apiSession. User rejected Master Password prompt.\n" + e);
- }
-
- this._apiSession = aValue;
- FireFM.obsService.notifyObservers(
- null, this.TOPIC_USER_AUTHENTICATION, this._userName);
- },
-
- /**
- * Checks the state of the Last.FM session cookie at startup.
- */
- _checkLoggedInState : function() {
- this._logger.trace("_checkLoggedInState");
-
- let cookieManager =
- Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
- let cookies = cookieManager.enumerator;
- let found = false;
- let cookie;
-
- while (cookies.hasMoreElements()) {
- cookie = cookies.getNext();
-
- if (this._isLastFMSessionCookie(cookie)) {
- found = true;
- break;
- }
- }
-
- if (found) {
- this._logger.debug("_checkLoggedInState. Logged in.");
- this._beginLogin();
- } else {
- this._logger.debug("_checkLoggedInState. Logged out.");
- FireFM.obsService.notifyObservers(
- null, this.TOPIC_USER_AUTHENTICATION, null);
- }
- },
-
- /**
- * Indicates if the given cookie is the Last.FM session cookie, used to know
- * if the user is logged in or not.
- * @param aCookie the cookie to check.
- * @return true if the cookie is the Last.FM session cookie, false otherwise.
- */
- _isLastFMSessionCookie : function(aCookie) {
- // XXX: there is no logging here for performance purposes.
- let isCookie =
- ((aCookie instanceof Ci.nsICookie) && (".last.fm" == aCookie.host) &&
- ("Session" == aCookie.name));
-
- return isCookie;
- },
-
- /**
- * Begins the login process for the extension.
- */
- _beginLogin : function() {
- this._logger.trace("_beginLogin");
-
- let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- let that = this;
-
- this._userName = null;
- this._apiSession = null;
- // XXX: we need a timeout here for 2 reasons: (1) there are some random
- // problems occurring in general right after we check the cookie, and (2)
- // we experience a similar problem consistently after returning from Private
- // Browsing mode.
- timer.initWithCallback(
- { notify : function() { that._getSession(); } }, GET_SESSION_TIMEOUT,
- Ci.nsITimer.TYPE_ONE_SHOT);
-
- // we don't need to use the special login page for users that were already
- // logged in.
- if (!this._useNormalLogin) {
- this._useNormalLoginPref.value = true;
- this._useNormalLogin = true;
- }
- },
-
- /**
- * Requests a session from Last.fm to get the currently logged in user name.
- */
- _getSession : function() {
- this._logger.trace("_getSession");
-
- let that = this;
- let inputStream = this._convertToStream(PARAMS_GET_SESSION);
-
- this._sendRequest(
- URL_RPC, function(aEvent) { that._getSessionLoad(aEvent); },
- function(aEvent) { that._getSessionError(aEvent); }, null,
- true, inputStream);
- },
-
- /**
- * Load callback handler for the get session request.
- * @param aEvent the event that triggered this function.
- */
- _getSessionLoad : function(aEvent) {
- this._logger.trace("_getSessionLoad");
-
- try {
- let doc = aEvent.target.responseXML;
- let strings = doc.getElementsByTagName("string");
- let user = strings[0].textContent;
-
- this._logger.debug("_getSessionLoad. User: " + user);
-
- if ((0 < user.length) && ("LFM_ANON" != user)) {
- let apiSession;
-
- this._userName = user;
- apiSession = this.apiSession; // _username needs to be set to do this!
-
- if (null == apiSession) {
- let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
- let that = this;
-
- // XXX: we need a timeout in this case because we may be in the
- // middle of the login process, which may clear our notification
- // due to the tab changing location.
- timer.initWithCallback(
- { notify : function() { that._showAPINotification(); } },
- API_NOTIFICATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
- } else {
- FireFM.obsService.notifyObservers(
- null, this.TOPIC_USER_AUTHENTICATION, user);
- // request a Scrobble session.
- FireFM.Remote.scrobbleHandshake();
- }
- } else {
- this._userName = null;
- this._apiSession = null;
- FireFM.obsService.notifyObservers(
- null, this.TOPIC_USER_AUTHENTICATION, null);
- }
- } catch (e) {
- this._logger.error(
- "_getSessionLoad. Invalid data received: " +
- aEvent.target.responseText + "\nError:\n" + e);
- this._getSessionError(aEvent, aIsGetUser);
- }
- },
-
- /**
- * Makes the main window display the API permission notification, if
- * necessary. It will avoid doing it if the permission page is already open.
- */
- _showAPINotification : function() {
- this._logger.trace("_showAPINotification");
-
- let wm =
- Cc["@mozilla.org/appshell/window-mediator;1"].
- getService(Ci.nsIWindowMediator);
- let win = wm.getMostRecentWindow("navigator:browser");
- let contentDoc = win.gBrowser.contentDocument;
-
- // only show this notification when not displaying the permission page. This
- // can happen the first time the user clicks on the login button after
- // installing.
- if (!contentDoc || (this.URL_API_ACCESS != contentDoc.documentURI)) {
- // tell the user to give permission to Fire.fm.
- win.FireFMChrome.BrowserOverlay.showAPINotification();
- }
- },
-
- /**
- * Error callback handler for the get session request.
- * @param aEvent the event that triggered this function.
- */
- _getSessionError : function(aEvent) {
- this._logger.error("_getSessionError");
-
- this._userName = null;
- this._apiSession = null;
- FireFM.obsService.notifyObservers(
- null, this.TOPIC_USER_AUTHENTICATION, null);
- this._defaultError("getSession", aEvent);
- },
-
- /**
- * Sends an HTTP request. This is just an utility function to save some code
- * lines.
- * @param aURL the url to send the request to.
- * @param aLoadHandler the load callback handler. Can be null.
- * @param aErrorHandler the error callback handler. Can be null.
- * @param aHeaders object mapping that represents the headers to send. Can be
- * null or empty.
- * @param aIsPOST indicates if the method POST (true) or GET (false).
- * @param aPOSTString the string or stream to send through post (optional).
- */
- _sendRequest : function(
- aURL, aLoadHandler, aErrorHandler, aHeaders, aIsPOST, aPOSTString) {
- this._logger.trace("_sendRequest");
-
- let request =
- Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
-
- // add event handlers.
- request.QueryInterface(Ci.nsIDOMEventTarget);
-
- if (null != aLoadHandler) {
- request.addEventListener("load", aLoadHandler, false);
- }
-
- if (null != aErrorHandler) {
- request.addEventListener("error", aErrorHandler, false);
- }
-
- // prepare and send the request.
- request.QueryInterface(Ci.nsIXMLHttpRequest);
- request.open((aIsPOST ? "POST" : "GET"), aURL, true);
-
- if (null != aHeaders) {
- for (let header in aHeaders) {
- request.setRequestHeader(header, aHeaders[header]);
- }
- }
-
- if (aIsPOST) {
- request.send(aPOSTString);
- } else {
- request.send(null);
- }
- },
-
- /**
- * Converts the given string into a UTF-8 string that can be sent through POST
- * as if it were binary. This is required for several Last.fm calls.
- * @param aString the string to convert into a stream.
- * @return nsIInputStream for the given string.
- */
- _convertToStream : function(aString) {
- this._logger.trace("_convertToStream");
-
- let multiStream =
- Cc["@mozilla.org/io/multiplex-input-stream;1"].
- createInstance(Ci.nsIMultiplexInputStream);
- let converter =
- Cc["@mozilla.org/intl/scriptableunicodeconverter"].
- createInstance(Ci.nsIScriptableUnicodeConverter);
- let inputStream;
-
- converter.charset = "UTF-8";
- inputStream = converter.convertToInputStream(aString);
- multiStream.appendStream(inputStream);
-
- return multiStream;
- },
-
- /**
- * Default error callback handler for the asynchronous requests.
- * @param aSource a string that identifies the source of the error.
- * @param aEvent the event that triggered this function.
- */
- _defaultError : function(aSource, aEvent) {
- this._logger.debug("_defaultError");
-
- try {
- this._logger.error(
- "_defaultError. Source: " + aSource + ", status: " +
- aEvent.target.status + ", response: " + aEvent.target.responseText);
- } catch (e) {
- this._logger.error("_defaultError. Error:\n" + e);
- }
- },
-
- /**
- * Decodes a Base64 encoded string and returns the clear text version.
- * @param aEncodedString a Base64 encoded string.
- * @return clear text contents of the Base64 string.
- * @throws Exception if the input string is badly formatted.
- */
- _decode : function(aEncodedString) {
- this._logger.debug("_decode");
-
- let src = aEncodedString;
- let decoded = "";
- let pos = 0;
- let v1, v2, v3, v4, v5, v6, v7;
-
- while (pos < src.length) {
- v5 = BASE64.indexOf(src.charAt(pos++));
- v3 = BASE64.indexOf(src.charAt(pos++));
- v1 = BASE64.indexOf(src.charAt(pos++));
- v2 = BASE64.indexOf(src.charAt(pos++));
-
- v4 = v5 << 2 | v3 >> 4;
- v7 = (v3 & 15) << 4 | v1 >> 2;
- v6 = (v1 & 3) << 6 | v2;
- decoded += String.fromCharCode(v4);
-
- if (v1 != 64) {
- decoded += String.fromCharCode(v7);
- }
-
- if (v2 != 64) {
- decoded += String.fromCharCode(v6);
- }
- }
-
- return FireFM.decodeFMString(decoded);
- },
-
- /**
- * Observes notifications of cookie and track activity.
- * @param aSubject The object that experienced the change.
- * @param aTopic The topic being observed.
- * @param aData The data related to the change.
- */
- observe : function(aSubject, aTopic, aData) {
- // XXX: there is no logging here for performance purposes.
- if (TOPIC_COOKIE_CHANGED == aTopic) {
- switch (aData) {
- case "added":
- if (this._isLastFMSessionCookie(aSubject)) {
- this._logger.debug("observe. Logged in.");
- this._beginLogin();
- }
- break;
- case "deleted":
- if (this._isLastFMSessionCookie(aSubject)) {
- this._logger.debug("observe. Cookie deleted.");
- this._userName = null;
- this._apiSession = null;
- // We used to just log a user out in this case, but there seems to
- // be cases where the a 'Session' cookie is removed right after a
- // new one has been set.
- this._checkLoggedInState();
- }
- break;
- case "cleared":
- case "reload":
- this._logger.info("observe. Cookie list reset.");
- this._userName = null;
- this._apiSession = null;
- this._checkLoggedInState();
- break;
- }
- }
- }
- };
-
- /**
- * FireFM.Login constructor.
- */
- (function() {
- this.init();
- }).apply(FireFM.Login);
-